﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Build.Utilities;
using Microsoft.Build.Execution;
using System.IO;
using System.Globalization;

namespace Microsoft.VisualStudio.Project
{
	public class DesignTimeAssemblyResolution
	{
		private const string OriginalItemSpec = "originalItemSpec";
	
		private const string FoundAssemblyVersion = "Version";
		
		private const string HighestVersionInRedistList = "HighestVersionInRedist";
		
		private const string OutOfRangeDependencies = "OutOfRangeDependencies";

		private RarInputs rarInputs;

		public bool EnableLogging { get; set; }

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "GetFrameworkPaths")]
		public virtual void Initialize(ProjectNode projectNode)
		{
			if (projectNode == null)
			{
				throw new ArgumentNullException("projectNode");
			}

			if (projectNode.CallMSBuild("GetFrameworkPaths") != MSBuildResult.Successful)
			{
				throw new InvalidOperationException("Build of GetFrameworkPaths failed.");
			}

			this.rarInputs = new RarInputs(projectNode.CurrentConfig);
		}

		public virtual VsResolvedAssemblyPath[] Resolve(IEnumerable<string> assemblies)
		{
			if (assemblies == null)
			{
				throw new ArgumentNullException("assemblies");
			}

			// Resolve references WITHOUT invoking MSBuild to avoid re-entrancy problems.
			const bool projectDtar = true;
			var rar = new Microsoft.Build.Tasks.ResolveAssemblyReference();
			var engine = new MockEngine(EnableLogging);
			rar.BuildEngine = engine;

			// first set common properties/items then if projectDtar then set additional projectDtar properties
			ITaskItem[] assemblyItems = assemblies.Select(assembly => new TaskItem(assembly)).ToArray();
			rar.Assemblies = assemblyItems;
			rar.SearchPaths = rarInputs.PdtarSearchPaths;

			rar.TargetFrameworkDirectories = rarInputs.TargetFrameworkDirectories;
			rar.AllowedAssemblyExtensions = rarInputs.AllowedAssemblyExtensions;
			rar.TargetProcessorArchitecture = rarInputs.TargetProcessorArchitecture;
			rar.TargetFrameworkVersion = rarInputs.TargetFrameworkVersion;
			rar.TargetFrameworkMoniker = rarInputs.TargetFrameworkMoniker;
			rar.TargetFrameworkMonikerDisplayName = rarInputs.TargetFrameworkMonikerDisplayName;
			rar.TargetedRuntimeVersion = rarInputs.TargetedRuntimeVersion;
			rar.FullFrameworkFolders = rarInputs.FullFrameworkFolders;
			rar.LatestTargetFrameworkDirectories = rarInputs.LatestTargetFrameworkDirectories;
			rar.FullTargetFrameworkSubsetNames = rarInputs.FullTargetFrameworkSubsetNames;
			rar.FullFrameworkAssemblyTables = rarInputs.FullFrameworkAssemblyTables;
			rar.IgnoreDefaultInstalledAssemblySubsetTables = rarInputs.IgnoreDefaultInstalledAssemblySubsetTables;
			rar.ProfileName = rarInputs.ProfileName;

			rar.Silent = !this.EnableLogging;
			rar.FindDependencies = true;
			rar.AutoUnify = false;
			rar.FindSatellites = false;
			rar.FindSerializationAssemblies = false;
			rar.FindRelatedFiles = false;

			// This set needs to be kept in sync with the set of project instance data that
			// is populated into RarInputs
			if (projectDtar)
			{
				// set project dtar specific properties 
				rar.CandidateAssemblyFiles = rarInputs.CandidateAssemblyFiles;
				rar.StateFile = rarInputs.StateFile;
				rar.InstalledAssemblySubsetTables = rarInputs.InstalledAssemblySubsetTables;
				rar.TargetFrameworkSubsets = rarInputs.TargetFrameworkSubsets;
			}

			IEnumerable<VsResolvedAssemblyPath> results;

			try
			{
				rar.Execute();
				results = FilterResults(rar.ResolvedFiles).Select(pair => new VsResolvedAssemblyPath
				{
					bstrOrigAssemblySpec = pair.Key,
					bstrResolvedAssemblyPath = pair.Value,
				});
			}
			catch (Exception ex)
			{
				if (ErrorHandler.IsCriticalException(ex))
				{
					throw;
				}

				engine.RecordRARExecutionException(ex);
				results = Enumerable.Empty<VsResolvedAssemblyPath>();
			}
			finally
			{
				if (this.EnableLogging)
				{
					WriteLogFile(engine, projectDtar, assemblies);
				}
			}

			return results.ToArray();
		}

		private static IEnumerable<KeyValuePair<string, string>> FilterResults(IEnumerable<ITaskItem> resolvedFiles)
		{
			foreach (ITaskItem resolvedFile in resolvedFiles)
			{
				bool bAddResolvedAssemblyToResultList = true;

				// excludeVersionWarningsFromResult
				string foundAssemblyVersion = resolvedFile.GetMetadata(FoundAssemblyVersion);
				string highestVersionInRedist = resolvedFile.GetMetadata(HighestVersionInRedistList);

				Version asmVersion = null;
				bool parsedAsmVersion = Version.TryParse(foundAssemblyVersion, out asmVersion);

				Version redistVersion = null;
				bool parsedRedistVersion = Version.TryParse(highestVersionInRedist, out redistVersion);

				if ((parsedAsmVersion && parsedRedistVersion) && asmVersion > redistVersion)
				{
					// if the version of the assembly is greater than the highest version - for that assembly - found in 
					// the chained(possibly) redist lists; then the assembly does not belong to the target framework 
					bAddResolvedAssemblyToResultList = false;
				}

				// check outOfRangeDependencies
				string outOfRangeDependencies = resolvedFile.GetMetadata(OutOfRangeDependencies);
				if (!String.IsNullOrEmpty(outOfRangeDependencies))
				{
					// This metadata is a semi-colon delimited list of dependent assembly names which target
					// a higher framework. If this metadata is NOT EMPTY then
					// the current assembly does have dependencies which are greater than the current target framework

					// so let's exclude this assembly
					bAddResolvedAssemblyToResultList = false;
				}

				if (bAddResolvedAssemblyToResultList)
				{
					yield return new KeyValuePair<string, string>(resolvedFile.GetMetadata(OriginalItemSpec), resolvedFile.ItemSpec);
				}
			}
		}

		private static void WriteLogFile(MockEngine engine, bool projectDtar, IEnumerable<string> assemblies)
		{
			string logFilePrefix = projectDtar ? "P" : "G";

			string logFilePath = Path.Combine(Path.GetTempPath(), logFilePrefix + @"Dtar" + (Guid.NewGuid()).ToString("N", CultureInfo.InvariantCulture) + ".log");

			StringBuilder inputs = new StringBuilder();

			Array.ForEach<string>(assemblies.ToArray(), assembly => { inputs.Append(assembly); inputs.Append(";"); inputs.Append("\n"); });

			string logAssemblies = "Inputs: \n" + inputs.ToString() + "\n\n";

			string finalLog = logAssemblies + engine.Log;

			string[] finalLogLines = finalLog.Split(new char[] { '\n' });

			File.WriteAllLines(logFilePath, finalLogLines);
		}
		
		/// <summary>
		/// Engine required by RAR, primarily for collecting logs
		/// </summary>
		private class MockEngine : IBuildEngine
		{
			private int messages = 0;
			private int warnings = 0;
			private int errors = 0;
			private StringBuilder log = new StringBuilder();
			private readonly bool enableLog = false;

			internal MockEngine(bool enableLog)
			{
				this.enableLog = enableLog;
			}

			public void RecordRARExecutionException(Exception ex)
			{
				if (!enableLog) return;

				log.Append(String.Format(CultureInfo.InvariantCulture, "{0}", ex.ToString()));
			}

			public void LogErrorEvent(BuildErrorEventArgs eventArgs)
			{
				if (eventArgs == null)
				{
					throw new ArgumentNullException("eventArgs");
				}

				if (!enableLog) return;

				if (eventArgs.File != null && eventArgs.File.Length > 0)
				{
					log.Append(String.Format(CultureInfo.InvariantCulture, "{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber));
				}

				log.Append("ERROR ");
				log.Append(eventArgs.Code);
				log.Append(": ");
				++errors;

				log.AppendLine(eventArgs.Message);
			}

			public void LogWarningEvent(BuildWarningEventArgs eventArgs)
			{
				if (eventArgs == null)
				{
					throw new ArgumentNullException("eventArgs");
				}

				if (!enableLog) return;

				if (eventArgs.File != null && eventArgs.File.Length > 0)
				{
					log.Append(String.Format(CultureInfo.InvariantCulture, "{0}({1},{2}): ", eventArgs.File, eventArgs.LineNumber, eventArgs.ColumnNumber));
				}

				log.Append("WARNING ");
				log.Append(eventArgs.Code);
				log.Append(": ");
				++warnings;

				log.AppendLine(eventArgs.Message);
			}

			public void LogCustomEvent(CustomBuildEventArgs eventArgs)
			{
				if (eventArgs == null)
				{
					throw new ArgumentNullException("eventArgs");
				}

				if (!enableLog) return;

				log.Append(eventArgs.Message);
				log.Append("\n");
			}

			public void LogMessageEvent(BuildMessageEventArgs eventArgs)
			{
				if (eventArgs == null)
				{
					throw new ArgumentNullException("eventArgs");
				}

				log.Append(eventArgs.Message);
				log.Append("\n");

				++messages;
			}

			public bool ContinueOnError
			{
				get { return false; }
			}

			public string ProjectFileOfTaskNode
			{
				get { return String.Empty; }
			}

			public int LineNumberOfTaskNode
			{
				get { return 0; }
			}

			public int ColumnNumberOfTaskNode
			{
				get { return 0; }
			}

			internal string Log
			{
				get { return log.ToString(); }
			}

			public bool BuildProjectFile(string projectFileName, string[] targetNames, System.Collections.IDictionary globalProperties, System.Collections.IDictionary targetOutputs)
			{
				throw new NotImplementedException();
			}
		}

		/// <summary>
		/// Accesssor for RAR related properties in the projectInstance.
		/// See ResolveAssemblyReferennce task msdn docs for member descriptions
		/// </summary>
		private class RarInputs
		{
			#region private fields

			// RAR related property/item names etc
			private const string TargetFrameworkDirectory = "TargetFrameworkDirectory";
			private const string RegistrySearchPathFormat = "Registry:{0},{1},{2}{3}";
			private const string FrameworkRegistryBase = "FrameworkRegistryBase";
			private const string TargetFrameworkVersionName = "TargetFrameworkVersion";
			private const string AssemblyFoldersSuffix = "AssemblyFoldersSuffix";
			private const string AssemblyFoldersExConditions = "AssemblyFoldersExConditions";
			private const string AllowedReferenceAssemblyFileExtensions = "AllowedReferenceAssemblyFileExtensions";
			private const string ProcessorArchitecture = "ProcessorArchitecture";
			private const string TargetFrameworkMonikerName = "TargetFrameworkMoniker";
			private const string TargetFrameworkMonikerDisplayNameName = "TargetFrameworkMonikerDisplayName";
			private const string TargetedRuntimeVersionName = "TargetedRuntimeVersion";
			private const string FullFrameworkReferenceAssemblyPaths = "_FullFrameworkReferenceAssemblyPaths";
			private const string TargetFrameworkProfile = "TargetFrameworkProfile";

			private const string ProjectDesignTimeAssemblyResolutionSearchPaths = "ProjectDesignTimeAssemblyResolutionSearchPaths";
			private const string Content = "Content";
			private const string None = "None";
			private const string RARResolvedReferencePath = "ReferencePath";
			private const string IntermediateOutputPath = "IntermediateOutputPath";
			private const string InstalledAssemblySubsetTablesName = "InstalledAssemblySubsetTables";
			private const string IgnoreInstalledAssemblySubsetTables = "IgnoreInstalledAssemblySubsetTables";
			private const string ReferenceInstalledAssemblySubsets = "_ReferenceInstalledAssemblySubsets";
			private const string FullReferenceAssemblyNames = "FullReferenceAssemblyNames";
			private const string LatestTargetFrameworkDirectoriesName = "LatestTargetFrameworkDirectories";
			private const string FullFrameworkAssemblyTablesName = "FullFrameworkAssemblyTables";
			private const string MSBuildProjectDirectory = "MSBuildProjectDirectory";

			#endregion //private fields

			public string[] TargetFrameworkDirectories { get; private set; }
			public string[] AllowedAssemblyExtensions { get; private set; }
			public string TargetProcessorArchitecture { get; private set; }
			public string TargetFrameworkVersion { get; private set; }
			public string TargetFrameworkMoniker { get; private set; }
			public string TargetFrameworkMonikerDisplayName { get; private set; }
			public string TargetedRuntimeVersion { get; private set; }
			public string[] FullFrameworkFolders { get; private set; }
			public string ProfileName { get; private set; }
			public string[] PdtarSearchPaths { get; private set; }
			public string[] CandidateAssemblyFiles { get; private set; }
			public string StateFile { get; private set; }
			public ITaskItem[] InstalledAssemblySubsetTables { get; private set; }
			public bool IgnoreDefaultInstalledAssemblySubsetTables { get; private set; }
			public string[] TargetFrameworkSubsets { get; private set; }
			public string[] FullTargetFrameworkSubsetNames { get; private set; }
			public ITaskItem[] FullFrameworkAssemblyTables { get; private set; }
			public string[] LatestTargetFrameworkDirectories { get; private set; }

			#region constructors
			public RarInputs(ProjectInstance projectInstance)
			{
				// Run through all of the entries we want to extract from the project instance before we discard it to save memory
				TargetFrameworkDirectories = GetTargetFrameworkDirectories(projectInstance);
				AllowedAssemblyExtensions = GetAllowedAssemblyExtensions(projectInstance);
				TargetProcessorArchitecture = GetTargetProcessorArchitecture(projectInstance);
				TargetFrameworkVersion = GetTargetFrameworkVersion(projectInstance);
				TargetFrameworkMoniker = GetTargetFrameworkMoniker(projectInstance);
				TargetFrameworkMonikerDisplayName = GetTargetFrameworkMonikerDisplayName(projectInstance);
				TargetedRuntimeVersion = GetTargetedRuntimeVersion(projectInstance);
				FullFrameworkFolders = GetFullFrameworkFolders(projectInstance);
				LatestTargetFrameworkDirectories = GetLatestTargetFrameworkDirectories(projectInstance);
				FullTargetFrameworkSubsetNames = GetFullTargetFrameworkSubsetNames(projectInstance);
				FullFrameworkAssemblyTables = GetFullFrameworkAssemblyTables(projectInstance);
				IgnoreDefaultInstalledAssemblySubsetTables = GetIgnoreDefaultInstalledAssemblySubsetTables(projectInstance);
				ProfileName = GetProfileName(projectInstance);

				/*               
				 * rar.CandidateAssemblyFiles = rarInputs.CandidateAssemblyFiles;
				   rar.StateFile = rarInputs.StateFile;
				   rar.InstalledAssemblySubsetTables = rarInputs.InstalledAssemblySubsetTables;
				   rar.TargetFrameworkSubsets = rarInputs.TargetFrameworkSubsets;
				 */

				// This set needs to be kept in sync with the set of project instance data that
				// is passed into Rar
				PdtarSearchPaths = GetPdtarSearchPaths(projectInstance);

				CandidateAssemblyFiles = GetCandidateAssemblyFiles(projectInstance);
				StateFile = GetStateFile(projectInstance);
				InstalledAssemblySubsetTables = GetInstalledAssemblySubsetTables(projectInstance);
				TargetFrameworkSubsets = GetTargetFrameworkSubsets(projectInstance);
			}
			#endregion // constructors

			#region public properties

			#region common properties/items

			private string[] GetTargetFrameworkDirectories(ProjectInstance projectInstance)
			{
				if (TargetFrameworkDirectories == null)
				{
					string val = projectInstance.GetPropertyValue(TargetFrameworkDirectory).Trim();

					TargetFrameworkDirectories = val.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
						.Select(s => s.Trim())
						.Where(s => s.Length > 0)
						.ToArray();
				}

				return TargetFrameworkDirectories;
			}

			private static string[] GetAllowedAssemblyExtensions(ProjectInstance projectInstance)
			{
				string[] allowedAssemblyExtensions;

				string val = projectInstance.GetPropertyValue(AllowedReferenceAssemblyFileExtensions).Trim();

				allowedAssemblyExtensions = val.Split(';').Select(s => s.Trim()).ToArray();

				return allowedAssemblyExtensions;
			}

			private static string GetTargetProcessorArchitecture(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(ProcessorArchitecture).Trim();

				return val;
			}

			private static string GetTargetFrameworkVersion(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(TargetFrameworkVersionName).Trim();

				return val;
			}

			private static string GetTargetFrameworkMoniker(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(TargetFrameworkMonikerName).Trim();

				return val;
			}

			private static string GetTargetFrameworkMonikerDisplayName(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(TargetFrameworkMonikerDisplayNameName).Trim();

				return val;
			}

			private static string GetTargetedRuntimeVersion(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(TargetedRuntimeVersionName).Trim();

				return val;
			}

			private static string[] GetFullFrameworkFolders(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(FullFrameworkReferenceAssemblyPaths).Trim();

				string[] _fullFrameworkFolders = val.Split(';').Select(s => s.Trim()).ToArray();

				return _fullFrameworkFolders;
			}

			private static string[] GetLatestTargetFrameworkDirectories(ProjectInstance projectInstance)
			{
				IEnumerable<ITaskItem> taskItems = projectInstance.GetItems(LatestTargetFrameworkDirectoriesName);

				string[] latestTargetFrameworkDirectory = (taskItems.Select((Func<ITaskItem, string>)((item) => { return item.ItemSpec.Trim(); }))).ToArray();

				return latestTargetFrameworkDirectory;
			}

			private static string GetProfileName(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(TargetFrameworkProfile).Trim();

				return val;
			}
			#endregion //common properties/items

			#region project dtar specific properties/items

			private static string[] GetPdtarSearchPaths(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(ProjectDesignTimeAssemblyResolutionSearchPaths).Trim();

				string[] _pdtarSearchPaths = val.Split(';').Select(s => s.Trim()).ToArray();

				return _pdtarSearchPaths;
			}

			private static string[] GetCandidateAssemblyFiles(ProjectInstance projectInstance)
			{
				var candidateAssemblyFilesList = new List<ProjectItemInstance>();

				candidateAssemblyFilesList.AddRange(projectInstance.GetItems(Content));
				candidateAssemblyFilesList.AddRange(projectInstance.GetItems(None));
				candidateAssemblyFilesList.AddRange(projectInstance.GetItems(RARResolvedReferencePath));

				string[] candidateAssemblyFiles = candidateAssemblyFilesList.Select((Func<ProjectItemInstance, string>)((item) => { return item.GetMetadataValue("FullPath").Trim(); })).ToArray();

				return candidateAssemblyFiles;
			}

			private static string GetStateFile(ProjectInstance projectInstance)
			{
				string intermediatePath = projectInstance.GetPropertyValue(IntermediateOutputPath).Trim();

				intermediatePath = GetFullPathInProjectContext(projectInstance, intermediatePath);

				string stateFile = Path.Combine(intermediatePath, "DesignTimeResolveAssemblyReferences.cache");

				return stateFile;
			}

			private static ITaskItem[] GetInstalledAssemblySubsetTables(ProjectInstance projectInstance)
			{
				return projectInstance.GetItems(InstalledAssemblySubsetTablesName).ToArray();
			}

			private static bool GetIgnoreDefaultInstalledAssemblySubsetTables(ProjectInstance projectInstance)
			{
				bool ignoreDefaultInstalledAssemblySubsetTables = false;

				string val = projectInstance.GetPropertyValue(IgnoreInstalledAssemblySubsetTables).Trim();

				if (!String.IsNullOrEmpty(val))
				{
					if (val == Boolean.TrueString || val == Boolean.FalseString)
					{
						ignoreDefaultInstalledAssemblySubsetTables = Convert.ToBoolean(val, CultureInfo.InvariantCulture);
					}
				}

				return ignoreDefaultInstalledAssemblySubsetTables;
			}

			private static string[] GetTargetFrameworkSubsets(ProjectInstance projectInstance)
			{
				IEnumerable<ITaskItem> taskItems = projectInstance.GetItems(ReferenceInstalledAssemblySubsets);

				string[] targetFrameworkSubsets = (taskItems.Select((Func<ITaskItem, string>)((item) => { return item.ItemSpec.Trim(); }))).ToArray();

				return targetFrameworkSubsets;
			}

			private static string[] GetFullTargetFrameworkSubsetNames(ProjectInstance projectInstance)
			{
				string val = projectInstance.GetPropertyValue(FullReferenceAssemblyNames).Trim();

				string[] fullTargetFrameworkSubsetNames = val.Split(';').Select(s => s.Trim()).ToArray();

				return fullTargetFrameworkSubsetNames;
			}

			private static ITaskItem[] GetFullFrameworkAssemblyTables(ProjectInstance projectInstance)
			{
				return projectInstance.GetItems(FullFrameworkAssemblyTablesName).ToArray();
			}

			#endregion //project dtar specific properties/items

			#endregion // public properties

			#region private methods
			static string GetFullPathInProjectContext(ProjectInstance projectInstance, string path)
			{
				string fullPath = path;

				if (!Path.IsPathRooted(path))
				{
					string projectDir = projectInstance.GetPropertyValue(MSBuildProjectDirectory).Trim();

					fullPath = Path.Combine(projectDir, path);

					fullPath = Path.GetFullPath(fullPath);
				}

				return fullPath;
			}
			#endregion // private methods
		}
	}
}
